package com.ale.oxetool.service;

import com.ale.oxetool.exceptions.BusinessException;
import com.ale.oxetool.model.Phone;
import com.ale.oxetool.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by wgs on 4/19/2016.
 */

@Service
@Primary
public class ParallelPhoneServiceImplement implements PhoneService {

    @Value("${oxeserver.oxe1.host}")
    private String oxe1Host;
    @Value("${oxeserver.oxe2.host}")
    private String oxe2Host;

    @Value("${telnet.read.timeout}")
    private long readTimeOut = 1000L;

    @Value("${config.test.debug:false}")
    private boolean debug;

    @Value("${phoneGroupFile:}")
    private String phoneGroupFile;
    //Map key is mac address, value is another map of this phone properties
    private Map<String, Map<String, Object>> phonePropertiesMap;

    public void setReadTimeOut(long readTimeOut) {
        this.readTimeOut = readTimeOut;
    }

    /**
     * @ Map key is mac address, value is another map of this phone properties
     */
    // load rich properties from file
    private Map<String, Map<String, Object>> loadPhonePropertiesFromFile() throws IOException {
        Map<String, Map<String, Object>> phonePropertiesMap = new HashMap<>();
        if (phoneGroupFile.isEmpty()) {
            return phonePropertiesMap;
        }

        File propFile;
        propFile = new File(phoneGroupFile);
        if (!propFile.exists()) { // fallback to resource root
            URL resource = this.getClass().getResource("/" + phoneGroupFile);
            if (null == resource) {
                log.warn("phone group file doesn't exist at {} and {}", propFile.getAbsolutePath(),
                        new File(this.getClass().getResource("/").getFile(), phoneGroupFile).getAbsolutePath()
                );
                return phonePropertiesMap;
            }
            propFile = new File(this.getClass().getResource("/" + phoneGroupFile).getFile());
        }
        //parse file
        log.info("begin parse file {}", propFile.getAbsolutePath());
        BufferedReader br = new BufferedReader(new FileReader(propFile));
        String line;
        // first valid line regarded as header line
        boolean isHeaderLine = true;
        Map<String, Integer> headerNameIndex = new HashMap<>();
        Map<Integer, String> headerIndexName = new HashMap<>();
        while (null != (line = br.readLine())) {
            // start with # or ; are comment line
            line = line.trim();
            if (line.startsWith("#") || line.startsWith(";")) continue;
            String[] tokens = line.split("\\s+|,");
            if (isHeaderLine) {
                for (int i = 0; i < tokens.length; ++i) {
                    headerNameIndex.put(tokens[i], i);
                    headerIndexName.put(i, tokens[i]);
                }
                if (!headerNameIndex.containsKey("mac")) {
                    // group file no mac column exists
                    log.warn("File {} doesn't have mac column", propFile);
                    break;
                }
            } else {
                if (tokens.length != headerNameIndex.size()) {
                    log.warn("line {} tokens doesn't match header line, ignore", line);
                    continue;
                }
                Map<String, Object> dynaProps;
                for (int i = 0; i < tokens.length; ++i) {
                    dynaProps = phonePropertiesMap.computeIfAbsent(tokens[headerNameIndex.get("mac")], k -> new HashMap<>());
                    dynaProps.put(headerIndexName.get(i), tokens[i]);
                }
            }
            isHeaderLine = false;
        }
        br.close();
        return phonePropertiesMap;
    }

    private Map<String, Map<String, Object>> getPhonePropertiesMap() throws IOException {
        if (null == phonePropertiesMap) {
            phonePropertiesMap = loadPhonePropertiesFromFile();
        }
        return phonePropertiesMap;
    }

    @Autowired
    private RemoteCommandServicePool pool;

    @Autowired
    private Util util;
    private static Logger log = LoggerFactory.getLogger(ParallelPhoneServiceImplement.class);

    //Whether disable tftp2 when do a phone switch
    @Value("${switch.priorityTftp}")
    private int priorityTftp = -1;

//    @Resource
//    private Map<String, String> phoneQmcduPrefixMap;

    //    The following method just used for test without OXE server.
    private List<Phone> getTestPhones() throws IOException {
        List<Phone> phones = Arrays.asList(
                new Phone("33002", "00809fc1cec3", "192.168.0.1", "mmc", "33002", oxe1Host, true),
                new Phone("33003", "00809fc1cec4", "192.168.0.2", "mmc", "33003", oxe2Host, false),
                new Phone("33009", "00809fc1cef4", "192.168.0.10", "mmc", "33009", oxe2Host, false)
        );
        Map<String, Map<String, Object>> phonePropsMap = getPhonePropertiesMap();
        for (Phone phone : phones) {
            // rich phone other properties
            Map<String, Object> props = phonePropsMap.get(phone.getMac());
            if (null != props) {
                phone.setDynamicProperties(props);
            }
        }
        return phones;
    }

    @Override
    public List<Phone> getPhones() throws IOException {
        if (debug) {
            return getTestPhones();
        }
        return getOXEPhones();
    }


    private List<Phone> getOXEPhones() throws IOException {
        RemoteCommandService rcOXE1 = null;
        RemoteCommandService rcOXE2 = null;

        try {
            rcOXE1 = pool.getRCS1();
            rcOXE2 = pool.getRCS2();

            List<Phone> rcOXE1Phones = rcOXE1.getPhoneInfoFromOXE();
            List<Phone> rcOXE2Pphones = rcOXE2.getPhoneInfoFromOXE();

            List<Phone> phones = new ArrayList<>();
            phones.addAll(rcOXE1Phones);
            phones.addAll(rcOXE2Pphones);

            Map<String, Map<String, Object>> phonePropsMap = getPhonePropertiesMap();
            for (Phone phone : phones) {
                phone.setOXE1(rcOXE1.getHost().equals(phone.getOxeServerAddr()));
                // rich phone other properties
                Map<String, Object> props = phonePropsMap.get(phone.getMac());
                if (null != props) {
                    phone.setDynamicProperties(props);
                }
            }
            return phones;

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            releaseOXEConnection(rcOXE1);
            releaseOXEConnection(rcOXE2);
        }
        throw new IOException("Connection to OXE error");
    }

    @Override
    public boolean login() {
        return true;
    }

    @Override
    public String getHost(int index) {
        OXEServerSettings oxeServerSettings = pool.getOXESettings(index);
        if (null != oxeServerSettings) {
            return oxeServerSettings.getHost();
        }
        return "Unknown";
    }

    /**
     * Switch phone with out open phone telnet
     * The opening telnet can be done in a batch
     */
    public String switchPhoneWithoutOpenPhoneTelnet(Phone phone) throws IOException {

        Date start = new Date();
        Date begin = start;
        //             OP on target phone
        //            step2 telnet login phone and switch phone
        String targetOXE = phone.getOxeServerAddr().equals(oxe1Host) ? oxe2Host : oxe1Host;
        int targetTFTPNum = 0;
        int theOtherTFTPNum = 1;

        if (priorityTftp == 1) {
            targetTFTPNum = 1;
            theOtherTFTPNum = 0;
        }

        TelnetOXEConnection rcCmd = new TelnetOXEConnection(phone.getIpAddr());
        rcCmd.setReadTimeout(readTimeOut);

        String expectPrompt = TelnetOXEConnection.phonePrompt;

        log.debug("phone={}, targetTftp={}, the other tftp={}, priorityTftp={}", phone.getName(), targetTFTPNum, theOtherTFTPNum, priorityTftp);
        String result = rcCmd.sendCommand(String.format("config tftp%d %s 69\n", targetTFTPNum, targetOXE), expectPrompt);
        Date current = new Date();
        log.debug("config phone {} tftp{} to {} at {}, used {} seconds.\nReturn result: {}", phone.getName(), targetTFTPNum, targetOXE, current,
                ((current.getTime() - begin.getTime()) / 1000.0), result);
        begin = new Date();
        // config tftp2 to original tftp or disable tftp2
        String targetAddress = "255.255.255.255";
        if (0 == priorityTftp || 1 == priorityTftp) {
            targetAddress = phone.getOxeServerAddr();
        }
        result = rcCmd.sendCommand(String.format("config tftp%d %s 69\n", theOtherTFTPNum, targetAddress), expectPrompt);
        current = new Date();
        log.debug("config phone {} tftp{} to {} at {}, used {} seconds. \nReturn result: {}", phone.getName(), theOtherTFTPNum, targetAddress, current,
                ((current.getTime() - begin.getTime()) / 1000.0), result);
        begin = new Date();
        result = rcCmd.sendCommand("reset", null);

        //Phone already reboot, in such case disconnect is not needed
//        rcCmd.disconnect();
        current = new Date();
        log.debug("reset phone {} at {}, used {} seconds, total switch used {} seconds.",
                phone.getName(), current, ((current.getTime() - begin.getTime()) / 1000.0),
                ((current.getTime() - start.getTime()) / 1000.0)
        );

        return result;
    }

    private String openTelnetForPhone(Phone phone) {
        return openTelnetForPhones(Collections.singletonList(phone));
    }

    public String openTelnetForPhones(List<Phone> phones) {
        Date begin = new Date();
        StringBuilder result = new StringBuilder();
        List<String> targetPhones = phones.stream().map(Phone::getIpAddr).collect(Collectors.toList());
        log.debug("Starting switch phones: {} at: {}", targetPhones, begin);
        RemoteCommandService rcOXE1 = null;
        RemoteCommandService rcOXE2 = null;
        try {
            rcOXE1 = pool.getRCS1();
            rcOXE2 = pool.getRCS2();

            List<List<String>> oxeCmdsList = Arrays.asList(new ArrayList<>(), new ArrayList<>());
            int TELNET_TIME_OUT = 1440;

            List<String> cmds;
            for (Phone phone : phones) {
                cmds = phone.getOxeServerAddr().equals(rcOXE1.getHost()) ? oxeCmdsList.get(0) : oxeCmdsList.get(1);
                cmds.add(String.format("tool ippstat telnet d %s t %d", phone.getNumber(), TELNET_TIME_OUT));
            }

            for (int index = 0; index < oxeCmdsList.size(); ++index) {
                // open telnet for all phones that registered on OXE0/1
                List<String> oxeCmds = oxeCmdsList.get(index);
                if (!oxeCmds.isEmpty()) {
                    RemoteCommandService rcOXE = 0 == index ? rcOXE1 : rcOXE2;
                    result.append(rcOXE.sendCommand(String.join(";", oxeCmds)));
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.error("Open OXE server error({})!", e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            log.error("OXE command execute error({})!", e.getMessage());
        } finally {
            releaseOXEConnection(rcOXE1);
            releaseOXEConnection(rcOXE2);
        }

        Date current = new Date();
        log.debug("After open telnet port for phone {} at {}, cost {} seconds. Return result:\n{}", targetPhones, current,
                (current.getTime() - begin.getTime()) / 1000.0, result.toString());
        return result.toString();
    }

    @Override
    public String switchPhone(Phone phone) throws IOException {
        String result = "";
        result += openTelnetForPhone(phone);
        result += switchPhoneWithoutOpenPhoneTelnet(phone);
        return result;
    }

    private void releaseOXEConnection(RemoteCommandService rcOXE) {
        if (null == rcOXE) {
            return;
        }
        try {
            rcOXE.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String switchPhone(String targetPlatform, String targetNumber) throws IOException, BusinessException {
        log.info("WS Switch phone....Target platform={}, targetNumber={}", targetPlatform, targetNumber);
        List<Phone> phones = getPhones();

        Optional<Phone> optionalPhone = phones.stream().filter((phone -> phone.getNumber().endsWith(targetNumber))).findFirst();
        if (!optionalPhone.isPresent()) {
            throw new BusinessException(BusinessException.PHONE_NUMBER_NOT_EXISTS, "Phone number " + targetNumber + " doesn't exist.");
        }
        Phone targetPhone = optionalPhone.get();
        if (util.isSwitchDone(targetPhone, targetPlatform)) {
            throw new BusinessException(BusinessException.ALREADY_ON_TARGET_PLATFORM, "Phone " + targetNumber + " already on platform " + targetPlatform);
        }
        return switchPhone(targetPhone);
    }

    @Override
    public void closeOXEServers() {
//        connections are managed by service pool, so we do nothing here.
    }
}
